# ImageCache - Handles image file cacheing.
# Copyright (C) 2007 Lauri Taimila
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

__licence__ = "GPLv2"
__copyright__ = "2007, Lauri Taimila"
__author__ = "Lauri Taimila <lauri@taimila.com>"

import os
import time
import Image
import string
import logging
import datetime
from pysqlite2 import dbapi2 as sqlite

import utils.EXIF as EXIF
from backend.components.mediacache.cache import Cache
from utils.image_thumbnailer import ImageThumbnailer

class ImageCache(Cache):
    """
    Object from this class is responsible of updateing image cache as requested.
    
    ImageCache has a public interface that consists of 3 mehtods: addFile,
    removeFile and updateFile. All these methods get filename as a parameter.
    When ImageCache is called with filename it checks if the filename is
    supported format. This is done simply by checking the file extension.
    
    Supported file formats are: JPEG
    """
    # Image cache database file
    IMAGE_DB = os.path.expanduser('~/.entertainer/cache/image.db')
    
    # Supported file formats
    SUPPORTED_FILE_EXTENSIONS = ['jpg','jpeg']
    
    def __init__(self, logger, configuration):
        """
        Create a new ImageCache.
        
        Creates a new database if not already exists and opens a connection
        to it.
        """
        self.logger = logger
        self.configuraiton = configuration
        if not os.path.exists(self.IMAGE_DB):
            self._createImageCacheDatabase()
        self.db_conn = sqlite.connect(self.IMAGE_DB)
        self.db_cursor = self.db_conn.cursor()
    
    def clearCache(self):
        """
        Clean image cache completely.
        
        Clean cache database and remova all thumbnails.
        """
        thumb_path = os.path.expanduser('~/.entertainer/cache/thumbnails/image')
        thumbnails = os.listdir(thumb_path)
        for element in thumbnails:
            thumb_file = os.path.join(thumb_path, element)
            try:
                os.remove(thumb_file)
            except:
                self.logger.error("Media manager couldn't remove thumbnail : %s" % thumb_file)
        try:
            os.remove(self.IMAGE_DB)
            self._createImageCacheDatabase()
        except:
            raise IOError("Couldn't clear image cache")
        
    
    def addFile(self, filename):
        """
        Add image file to the cache. Do nothing if file is already cached.
        """
        try:
            filename = filename.encode('utf8')
            if not self.isFileInCache(filename) and self.isSupportedFormat(filename):
                # Do not add album thumbnail to images
                if filename[filename.rfind('/') +1:filename.rfind('.')] == ".entertainer_album":
                    return
                try:
                    self._addJPEGfile(filename)
                except Exception, e:
                    self.logger.error("Couldn't cache file: " + filename)
                    self.logger.debug("Exception: " + str(e.message))
        except Exception, e:
            self.__logger.error("Problem With " + filename)
            self.__logger.error("Exception: " + e.message)
        
    def removeFile(self, filename):
        """
        Remove image file from the cache. Do nothing if file is not in cache.
        """
        # Remove image file
        if self.isFileInCache(filename):
            self.db_cursor.execute("""SELECT hash 
                                        FROM image 
                                        WHERE filename=:fn""", 
                                        { "fn" : filename})
            result = self.db_cursor.fetchall()
            if len(result) > 0:
                thumb = os.path.expanduser('~/.entertainer/cache/thumbnails/image/'+ result[0][0] + '.jpg')
                try:
                    os.remove(thumb)
                except:
                    self.logger.error("Couldn't remove thumbnail: " + thumb)
                self.db_cursor.execute("""DELETE 
                                            FROM image 
                                            WHERE filename=:fn""",
                                            { "fn" : filename })
                self.db_conn.commit()
         
    def updateFile(self, filename):
        """Update image file that is already in the cache."""
        if self.isFileInCache(filename):
            self.removeFile(filename)
            self.addFile(filename)
        #FIXME: This is maybe too expensive method for this purpose! Try to optimise it.

    def addDirectory(self, path):
        """
        Adds a new directory to the cache. Sub directories are
        added recursively and all files in them.
        """
        if not os.path.isdir(path) or not os.path.exists(path):
            self.logger.error("Adding a directory to the image cache failed. Path doesn't exist: " + path)
        else:
            #print "Add directory: " + str(path) #FIXME: REMOVE THIS DEBUG OUTPUT
            for root, dirs, files in os.walk(path):
                if not self.isDirectoryInCache(root):
                    self._addAlbum(root)
                for name in files:
                    if self.isSupportedFormat(name):
                        self.addFile(os.path.join(root, name))
                        time.sleep(float(self.SLEEP_TIME_BETWEEN_FILES) / 1000)
                    
    def removeDirectory(self, path):
        """
        Removes directory from the cache. Also removes all subdirectories
        and all files in them.
        
        @param path - Absolute path
        """
        thumb_path = os.path.expanduser('~/.entertainer/cache/thumbnails/image')
            
        # Remove image file thumbnails
        self.db_cursor.execute("""SELECT hash 
                                    FROM image 
                                    WHERE filename LIKE '""" + path + "%'")
        for row in self.db_cursor:
            thumb_file = row[0] + ".jpg"
            try:
                os.remove(os.path.join(thumb_path, thumb_file))
            except:
                pass
        
        # Remove folder thumbnails
        self.db_cursor.execute("""SELECT hash 
                                    FROM album 
                                    WHERE path LIKE '""" + path + "%'")
        for row in self.db_cursor:
            thumb_file = row[0] + ".jpg"
            try:
                os.remove(os.path.join(thumb_path, thumb_file))
            except:
                pass
        
        # Clean cache database
        self.db_cursor.execute("DELETE FROM album WHERE path LIKE '" + path + "%'")
        self.db_cursor.execute("DELETE FROM image WHERE album_path LIKE '" + path + "%'")
        self.db_conn.commit()
        
    def updateDirectory(self, path):
        """
        Update directory.
        """
        self.removeDirectory(path)
        self.addDirectory(path)
        #FIXME: This is very expensive method to call! Try to optimise it.
        
    def isFileInCache(self, filename):
        """Check if file is already in cache. Returns boolean value."""
        self.db_cursor.execute("""SELECT * 
                                    FROM image 
                                    WHERE filename=:fn""", { "fn" : filename })
        result = self.db_cursor.fetchall()
        if len(result) == 0:
            return False
        else:
            return True
            
    def isDirectoryInCache(self, path):
        """Check if album is already in cache. Returns boolean value."""
        self.db_cursor.execute("""SELECT * 
                                    FROM album 
                                    WHERE path=:p""", { "p" : path})
        result = self.db_cursor.fetchall()
        if len(result) == 0:
            return False
        else:
            return True

    def isSupportedFormat(self, filename):
        """Check if file is supported."""
        if filename[filename.rfind('.') + 1 :].lower() in self.SUPPORTED_FILE_EXTENSIONS:
            return True
        else:
            return False
                    
# ------------------------------------------------------------------------------
# PRIVATE METHODS ARE BELOW                                                     
# ------------------------------------------------------------------------------

    def _createImageCacheDatabase(self):
        """Creates a image cache database file."""
        db_conn = sqlite.connect(self.IMAGE_DB)
        db_cursor = db_conn.cursor()
        db_cursor.execute("""CREATE TABLE image(
                             filename TEXT,
                             album_path TEXT, 
                             title TEXT,
                             description TEXT, 
                             date DATE, 
                             time TIME,
                             width INTEGER,
                             height INTEGER,
                             filesize LONG,
                             hash VARCHAR(32),
                             PRIMARY KEY(filename))""")
                                
        db_cursor.execute("""CREATE TABLE album(
                             path TEXT,
                             title TEXT,
                             description TEXT,
                             hash VARCHAR(32),
                             PRIMARY KEY(path))""")
        db_conn.commit()
        db_conn.close()
        self.logger.debug("ImageCache database created successfully")

    def _addAlbum(self, path):
        """
        Create a new album into image cache. Folders are handled as albums.
        Nested folders are not nested in database! All albums are on top level.
        """
        
        album_info = os.path.join(path, ".entertainer_album.info")
        album_thumb = os.path.join(path, ".entertainer_album.jpg")

        # Get album information
        if os.path.exists(album_info):
            try:
                inf_f = open(album_info)
                a_title = inf_f.readline()[6:]
                a_description = inf_f.readline()[12:]
            except:
                a_title = path[path.rfind('/')+1:].replace('_',' ').title()
                a_description = ""
        else:
            a_title = path[path.rfind('/')+1:].replace('_',' ').title()
            a_description = ""
            
        #FIXME: If not found we could generate fancy album thumb
        if os.path.exists(album_thumb):
            thumbnailer = ImageThumbnailer(album_thumb)
            thumbnailer.create_thumbnail()
            a_hash = thumbnailer.get_hash()
            del thumbnailer #FIXME: Does this make any sense?
        else:
            a_hash = ""
            
        album_row = (path, a_title, a_description, a_hash)
        self.db_cursor.execute("""INSERT INTO album(path, title, description, hash) 
                                    VALUES(?,?,?,?)""", album_row)
        self.db_conn.commit()
        #print "Album added to cache: " + a_title

    def _addJPEGfile(self, filename):
        """
        Add JPEG image to the image cache. Raises exception if adding fails.
        
        Process:
            - Open file
            - Get image date and time
            - Get image title and description
            - Get image size
            - Generate thumbnail / get hash from thumbnailer
            - Insert data to image cache database
        """
        try:
            f = open(filename, 'rb')
        except:
            raise Exception("Couldn't open image file!")
        
        tags=EXIF.process_file(f)

        # Get image's taken date and time
        if tags.has_key("EXIF DateTimeOriginal"):
            timestamp = string.split(str(tags['EXIF DateTimeOriginal']))
            timestamp[0] = timestamp[0].replace(':','-')
        elif tags.has_key("Image DateTime"):
            timestamp = string.split(str(tags['Image DateTime']))
            timestamp[0] = timestamp[0].replace(':','-')
        else: # If no metadata exists then use file's 'created' date
            tmp = datetime.datetime.fromtimestamp(os.stat(filename)[-1])
            timestamp = [str(tmp.year) + "-" + str(tmp.month) + "-" + str(tmp.day),
                         str(tmp.hour) + ":" + str(tmp.minute) + ":" + str(tmp.second)]
        
        # Prefer explicite metadata file. If that fails read EXIF data
        try:
            title, description = self._readMetadataFile(filename)
        except:
            # Generate name from filename
            tmp = filename[filename.rfind('/') + 1 : filename.rfind('.')]
            title = tmp.replace('_',' ').title() # Make title more attractive
            if tags.has_key("EXIF UserComment"):
                description = str(tags['EXIF UserComment'])
            else:
                description = "" # No description for this image file
        
        # Get image size. Prefer EXIF metadata, fall back to Image library
        # FIXME: is EXIF faster that Image library? Which one should be default?
        if tags.has_key("EXIF ExifImageWidth") and tags.has_key("EXIF ExifImageLength"):
            width = str(tags['EXIF ExifImageWidth'])
            height = str(tags['EXIF ExifImageLength'])
        else:
            im = Image.open(filename)
            width, height = im.size
        
        # Create thumbnail and return hash
        thumbnailer = ImageThumbnailer(filename)
        thumbnailer.create_thumbnail()
        thumb_hash = thumbnailer.get_hash()
        del thumbnailer
        album_path = filename[:filename.rfind('/')]
        
        db_row = (filename,                 # Filename (full path)
                  title,                    # Title of the image
                  description,              # Description of the image
                  timestamp[0],             # Image's taken date
                  timestamp[1],             # Image's taken time
                  width,                    # Image's width
                  height,                   # Image's height
                  os.path.getsize(filename),# Image file size in bytes
                  thumb_hash,               # Thumbnail hash (hash of the filename)
                  album_path)               # Path of the album (folder of this image)

        self.db_cursor.execute("""INSERT INTO image(filename, 
                                                      title, 
                                                      description, 
                                                      date, 
                                                      time, 
                                                      width, 
                                                      height, 
                                                      filesize, 
                                                      hash, 
                                                      album_path) 
                                     VALUES(?,?,?,?,?,?,?,?,?,?)""", db_row)
        self.db_conn.commit()
           
    def _readMetadataFile(self, filename):
        """
        Reads image title and description from explicite metadata file if
        the file exists. If file doesn't exist, this method throws an 
        exception, which indicates to the caller that metadata should be
        received from some place else.
        """
        # Generate metadata filename
        abs_path = filename[:filename.rfind('/') + 1] # Cut filename off
        img_file = filename[filename.rfind('/') + 1:] # Cut path off
        metadata_file = abs_path + "." + img_file[:img_file.rfind('.')] +".info"
        
        if os.path.exists(metadata_file):
            try:
                info_file = open(metadata_file)
                title = info_file.readline()[6:]
                description = info_file.readline()[12:]
                return title, description
            except:
                raise Exception("Couldn't read existing metadata file.")
        else:
            raise Exception("Metadata file doesn't exist.")

